package com.cm.kits;

import com.cm.kits.oss.OssKits;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.CookieManager;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.*;

/**
 * 基于 jdk HttpClient 的网络访问工具
 *
 * @author LWang 2023.02.01
 * @since 1.0.0
 */
@Slf4j
public final class HttpTool {
    private final HttpClient httpClient;

    private final CookieManager cookieManager;

    public HttpTool() throws NoSuchAlgorithmException, KeyManagementException {
        // 取消主机签名验证
        System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");
        // 信任证书
        var trustAllCertificates = new TrustManager[]{new InnerX509TrustManager()};
        var sslParameters = new SSLParameters();

        sslParameters.setEndpointIdentificationAlgorithm(StringUtils.EMPTY);
        var sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCertificates, new SecureRandom());
        cookieManager = new CookieManager();
        httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(WAIT_TIMEOUT))
                .cookieHandler(cookieManager)
                .followRedirects(HttpClient.Redirect.NEVER)
                .sslContext(sslContext)
                .sslParameters(sslParameters)
                .build();
    }


    /**
     * 创建请求 Builder 对象
     *
     * @param uri     请求 URI
     * @param headers 请求头
     * @return Request Builder 对象
     */
    private HttpRequest.Builder createRequestBuilder(URI uri, Map<String, String> headers) {
        if (Objects.isNull(uri)) {
            throw new IllegalArgumentException("缺少必要请求参数：uri");
        }
        var builder = HttpRequest.newBuilder().uri(uri).expectContinue(false);
        if (!Objects.isNull(headers)) {
            headers.forEach(builder::setHeader);
        }
        return builder;

    }

    /**
     * 拼装查询字符串
     *
     * @param parameters 请求参数
     * @return 拼装后的查询字符串
     */
    private String assemblyParameters(List<Pair<String, String>> parameters) {
        if (Objects.isNull(parameters) || parameters.isEmpty()) {
            return StringUtils.EMPTY;
        }
        final var arrays = new ArrayList<String>(parameters.size());
        parameters.forEach(pair -> arrays.add(String.format("%s%c%s", pair.getKey(), EQUAL_SIGN, URLEncoder.encode(pair.getValue(), StandardCharsets.UTF_8))));
        return StringUtils.join(arrays, AND_MARK);
    }

    /**
     * 拼装文件上传请求参数
     *
     * @param parameters 普通参数
     * @param files      文件参数
     * @return 拼装后的结果
     */
    private Pair<String, byte[]> assemblyParameters(List<Pair<String, String>> parameters, List<Pair<String, File>> files) throws IOException {
        if (!Objects.isNull(files)) {
            var build = MultipartEntityBuilder.create();
            if (!Objects.isNull(parameters)) {
                parameters.forEach(pair -> build.addTextBody(pair.getKey(), pair.getValue()));
            }
            files.forEach(pair -> {
                var key = pair.getKey();
                var file = pair.getValue();
                if (file.exists() && file.isFile()) {
                    build.addBinaryBody(key, file);
                }
            });
            var entities = build.build();
            try (var out = new ByteArrayOutputStream()) {
                entities.writeTo(out);
                return Pair.of(entities.getContentType().getValue(), out.toByteArray());
            }
        } else {
            return Pair.of(ContentType.APPLICATION_FORM_URLENCODED.getMimeType(), assemblyParameters(parameters).getBytes());
        }
    }

    /**
     * 拼装请求 URI
     *
     * @param uri        原始 URI
     * @param parameters 查询参数
     * @return 拼装好的 URI
     */
    private URI assemblyUri(String uri, List<Pair<String, String>> parameters) {
        if (StringUtils.isBlank(uri)) {
            throw new IllegalArgumentException("缺少必要参数：url");
        }
        if (Objects.isNull(parameters) || parameters.isEmpty()) {
            return URI.create(uri);
        }
        // 拼装查询字符串

        return URI.create(String.format("%s%c%s", uri, uri.indexOf(QUERY_MARK) < 0 ? QUERY_MARK : AND_MARK, assemblyParameters(parameters)));
    }

    /**
     * 添加请求 Cookie
     *
     * @param uri     URI
     * @param cookies 要添加的 Cookies
     * @return URI
     */
    @SafeVarargs
    private URI putCookies(URI uri, Map<String, List<String>>... cookies) throws IOException {
        if (Objects.isNull(uri)) {
            throw new IllegalArgumentException("缺少必要的请求参数：uri");
        }
        if (!Objects.isNull(cookies)) {
            for (var cookie : cookies) {
                cookieManager.put(uri, cookie);
            }
        }
        return uri;
    }

    /**
     * 发送 GET 请求
     *
     * @param uri 请求地址
     * @return 请求响应结果（http 响应码，http 响应结果）
     */
    public Pair<Integer, String> get(String uri) throws IOException, InterruptedException {
        return get(uri, null, null, null);
    }

    /**
     * 发送 GET 请求
     *
     * @param uri             请求地址
     * @param queryParameters 请求参数
     * @return 请求响应结果（http 响应码，http 响应结果）
     */
    public Pair<Integer, String> get(String uri, List<Pair<String, String>> queryParameters) throws IOException, InterruptedException {
        return get(uri, queryParameters, null, null);
    }

    /**
     * 发送 GET 请求
     *
     * @param uri             请求地址
     * @param queryParameters 请求参数
     * @param headers         请求头
     * @param cookies         请求 Cookies
     * @return 请求响应结果（http 响应码，http 响应结果）
     */
    public Pair<Integer, String> get(
            String uri,
            List<Pair<String, String>> queryParameters,
            Map<String, String> headers,
            Map<String, List<String>>[] cookies
    ) throws IOException, InterruptedException {
        var builder = createRequestBuilder(putCookies(assemblyUri(uri, queryParameters), cookies), headers);
        var httpClient = this.httpClient;
        var request = builder.GET();
        var response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString());
        return Pair.of(response.statusCode(), response.body());
    }


    /**
     * 使用 application/x-www-form-urlencoded 进行 POST 提交
     *
     * @param uri        请求 URI
     * @param parameters 查询参数（FORM 部分）
     * @return 请求响应结果（http 响应码，http 响应结果）
     * @throws IOException          异常时抛出
     * @throws InterruptedException 异常时抛出
     */
    public Pair<Integer, String> post(
            String uri,
            List<Pair<String, String>> parameters
    ) throws IOException, InterruptedException {
        return post(uri, null, parameters, null, null, null);
    }

    /**
     * 使用 application/x-www-form-urlencoded 进行 POST 提交
     *
     * @param uri             请求 URI
     * @param queryParameters 查询参数（查询字符串部分）
     * @param parameters      查询参数（FORM 部分）
     * @return 请求响应结果（http 响应码，http 响应结果）
     * @throws IOException          异常时抛出
     * @throws InterruptedException 异常时抛出
     */
    public Pair<Integer, String> post(
            String uri,
            List<Pair<String, String>> queryParameters,
            List<Pair<String, String>> parameters
    ) throws IOException, InterruptedException {
        return post(uri, queryParameters, parameters, null, null, null);
    }

    /**
     * 使用 application/x-www-form-urlencoded 进行 POST 提交
     *
     * @param uri             请求 URI
     * @param queryParameters 查询参数（查询字符串部分）
     * @param parameters      查询参数（FORM 部分）
     * @param files           查询参数，文件参数
     * @param headers         请求头
     * @param cookies         请求 Cookies
     * @return 请求响应结果（http 响应码，http 响应结果）
     * @throws IOException          异常时抛出
     * @throws InterruptedException 异常时抛出
     */
    public Pair<Integer, String> post(
            String uri,
            List<Pair<String, String>> queryParameters,
            List<Pair<String, String>> parameters,
            List<Pair<String, File>> files,
            Map<String, String> headers,
            Map<String, List<String>>[] cookies
    ) throws IOException, InterruptedException {
        if (Objects.isNull(headers)) {
            headers = new HashMap<>(1);
        }
        var entities = assemblyParameters(parameters, files);
        headers.put("Content-Type", entities.getKey());
        var builder = createRequestBuilder(putCookies(assemblyUri(uri, queryParameters), cookies), headers);
        var httpClient = this.httpClient;
        var request = builder.POST(HttpRequest.BodyPublishers.ofByteArray(entities.getValue()));
        var response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString());
        return Pair.of(response.statusCode(), response.body());
    }

    /**
     * 下载文件，适用于简单的 GET 下载
     *
     * @param uri     文件下载地址
     * @param fileExt 文件扩展名，不提供默认为 tmp
     * @return 请求响应结果（http 响应码，http 响应结果）
     * @throws IOException          异常时抛出
     * @throws InterruptedException 异常时抛出
     */
    public Pair<Integer, Path> download(String uri, String fileExt) throws IOException, InterruptedException {
        return download(Method.GET, uri, fileExt, null, null, null, null, null);
    }

    /**
     * 下载文件
     *
     * @param method          请求方式
     * @param uri             请求地址
     * @param fileExt         文件扩展名
     * @param queryParameters 请求参数（查询字符串部分）
     * @param parameters      请求参数（POST 请求部分）
     * @param files           查询参数，文件参数
     * @param headers         请求头信息
     * @param cookies         请求 cookies
     * @return 请求响应结果（http 响应码，http 响应结果）
     * @throws IOException          异常时抛出
     * @throws InterruptedException 异常时抛出
     */
    public Pair<Integer, Path> download(
            Method method,
            String uri,
            String fileExt,
            List<Pair<String, String>> queryParameters,
            List<Pair<String, String>> parameters,
            List<Pair<String, File>> files,
            Map<String, String> headers,
            Map<String, List<String>>[] cookies
    ) throws IOException, InterruptedException {
        if (Objects.isNull(headers)) {
            headers = new HashMap<>(1);
        }
        var entities = assemblyParameters(parameters, files);
        headers.put("Content-Type", entities.getKey());
        var builder = createRequestBuilder(putCookies(assemblyUri(uri, queryParameters), cookies), headers);
        var httpClient = this.httpClient;
        HttpRequest.Builder request;
        switch (method) {
            case GET -> request = builder.GET();
            case POST -> request = builder.POST(HttpRequest.BodyPublishers.ofByteArray(entities.getValue()));
            default -> throw new IllegalStateException("暂不支持的请求方式: " + method.getType());
        }

        var path = Path.of(
                "%s%s_%s.%s".formatted(
                        FileUtils.getTempDirectoryPath(),
                        DateTimeKits.currentDay(),
                        UUID.randomUUID().toString(),
                        (StringUtils.isBlank(fileExt) ? "tmp" : fileExt)
                )
        );
        var response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofFile(path));
        return Pair.of(response.statusCode(), response.body());
    }

    /* --------------------------- 静态成员区 --------------------------- */

    private final static char EQUAL_SIGN = '=';
    private final static char AND_MARK = '&';
    private final static char QUERY_MARK = '?';
    /**
     * 请求等待超时时间（单位：秒）
     */
    private final static long WAIT_TIMEOUT = 10;

    public enum Method {
        /**
         *
         */
        POST("POST"),
        /**
         *
         */
        GET("GET");
        final String type;

        public String getType() {
            return type;
        }

        Method(String type) {
            this.type = type;
        }
    }

    private static class InnerX509TrustManager implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    /**
     * 创建网络工具
     *
     * @return 网络工具实例
     * @throws NoSuchAlgorithmException 证书处理异常
     * @throws KeyManagementException   证书处理异常
     */
    public static HttpTool build() throws NoSuchAlgorithmException, KeyManagementException {
        return new HttpTool();
    }

    public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, IOException, InterruptedException {
        var url = "https://etax.tianjin.chinatax.gov.cn/WsbsWebBn/wsbsWszmAction_showPdf.do?applicationId=k95oMKvY5GnrX0y6afyxXeRTnybnoa7zq4tPi9AvJ9kdswux79%2BXaGF0w9ZVleKz&zlId=N5nYxzZydYbil0OmVOKYig%3D%3D";
        var httpClient = HttpTool.build();
        var downloadInfo = httpClient.download(Method.GET, url, "pdf", null, null, null, null, null);
        System.out.println(downloadInfo);
        System.out.println(OssKits.upload(downloadInfo.getValue().toFile()));
    }
}
